#
# Copyright (c) 2023 supercell
#
# SPDX-License-Identifier: BSD-3-Clause
#

module Luce
  # Matches autolinks like `http://foo.com` and `foo@bar.com`
  class AutolinkExtensionSyntax < InlineSyntax
    @@link_pattern : String = "" +
      # Autolinks can only come at the beginning of a line, after whitespace,
      # or any of the delimiting characters *, _, ~, and (.
      # NOTE: Disable this part for compatibility with dart-lang/markdown.
      #       May be re-enabled later.
      # %q{(?<=^|[\s*_~(>])} +

      # An extended url autolink will be recognised when one of the schemes
      # http://, or https://, followed by a valid domain. See
      # https://github.github.com/gfm/#extended-url-autolink.
      %q{(?:(?:https?|ftp):\/\/|www\.)} +
      # A valid domain consists of segments of alphanumeric characters,
      # underscores (_) and hyphens (-) separated by periods (.). There must
      # be at least one period, and no underscores may be present in the last
      # two segments of the domain. See
      # https://github.github.com/gfm/#valid-domain
      %q{(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)} +
      # After a valid domain, zero or more non-space non-< characters may
      # follow
      %q{[^\s<]*} +
      # Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will
      # not be considered part of the autolink, though they may be included in
      # the interior of the link. See
      # https://github.github.com/gfm/#extended-autolink-path-validation.
      # NOTE: Disable this part for compatibility with dart-lang/markdown.
      #       May be re-enabled later.
      # "(?<![?!.,:*_~])"
      %q{[^\s<?!.,:*_~]}

    # An extended email autolink, see
    # https://github.github.com/gfm/#extended-email-autolink
    @@email_pattern =
      %q{[-_.+a-z0-9]+@(?:[-_a-z0-9]+\.)+[-_a-z0-9]*[a-z0-9]}

    def initialize
      super("(#{@@link_pattern})|(#{@@email_pattern})", case_sensitive: false)
    end

    def matches?(parser : InlineParser, start_match_pos : Int32? = nil) : Bool
      start_match_pos ||= parser.pos
      start_match = pattern.match(parser.source, start_match_pos)
      if start_match.nil? || start_match.begin != start_match_pos
        return false
      end

      # When it is a link and it is not at the beginning of a line, or preceded
      # by whitespace, `*`, `_`, `~`, `(`, or `>`, it is invalid. See
      # https://github.github.io/gfm/#autolinks-extension-.
      if start_match[1]? != nil && parser.pos > 0
        preceded_by = parser.char_at(parser.pos - 1).chr
        valid_preceding_chars = ['\n', ' ', '*', '_', '~', '(', '>']
        return false unless valid_preceding_chars.includes? preceded_by
      end

      # When it is an email link and followed by `_` or `-`, it is invalid. See
      # https://github.github.io/gfm/#example-633
      # ameba:disable Lint/NotNil
      if start_match[2]? != nil && parser.source.size > start_match.end.not_nil!
        # ameba:disable Lint/NotNil
        followed_by = parser.char_at(start_match.end.not_nil!).chr
        invalid_following_chars = ['_', '-']
        return false if invalid_following_chars.includes? followed_by
      end
      parser.write_text
      on_match(parser, start_match)
    end

    def on_match(parser : InlineParser, match : Regex::MatchData) : Bool
      consume_length : Int32

      is_email_link = match[2]? != nil
      if is_email_link
        consume_length = match[0].size
      else
        consume_length = get_consume_length(match[0])
      end

      text = match[0][0...consume_length]
      text = parser.encode_html? ? Luce.escape_html(text) : text

      destination = text
      if is_email_link
        destination = "mailto:#{destination}"
      elsif destination[0] == 'w'
        # When there is no scheme specified, insert the scheme `http`.
        destination = "http://#{destination}"
      end

      anchor = Element.text("a", text)
      anchor.attributes["href"] = DartURI.encode_full(destination)

      parser.add_node(anchor)
      parser.consume(consume_length)
      true
    end

    private def get_consume_length(text : String) : Int32
      excluded_length = 0

      if text.ends_with? ")"
        # ameba:disable Lint/NotNil
        match = /(\(.*)?(\)+)$/.match(text).not_nil!

        if match[1]?.nil?
          excluded_length = match[2].size
        else
          paren_count = 0
          text.each_codepoint do |char|
            if char == Charcode::LPAREN
              paren_count += 1
            elsif char == Charcode::RPAREN
              paren_count -= 1
            end
          end
          excluded_length = paren_count.abs if paren_count < 0
        end
      elsif text.ends_with? ";"
        match = /&[0-9a-z]+;$/.match(text)
        unless match.nil?
          excluded_length = match[0].size
        end
      end

      text.size - excluded_length
    end
  end
end
